package com.nova41.bukkitdev.slr;

import com.nova41.bukkitdev.slr.command.CommandManager;
import com.nova41.bukkitdev.slr.listener.PlayerAttackAngleLogger;
import com.nova41.bukkitdev.slr.model.LVQNeuralNetwork;
import com.nova41.bukkitdev.slr.model.LVQNeuralNetworkPredictResult;
import com.nova41.bukkitdev.slr.model.LVQNeuralNetworkSummary;
import com.nova41.bukkitdev.slr.model.LabeledData;
import com.nova41.bukkitdev.slr.util.SLFiles;
import com.nova41.bukkitdev.slr.util.SLMaths;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.bukkit.ChatColor;
import org.bukkit.Location;
import org.bukkit.command.CommandSender;
import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.entity.Villager;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;

import java.io.File;
import java.io.IOException;
import java.math.RoundingMode;
import java.util.*;
import java.util.function.Consumer;

/**
 * SnowLeopardR - a rebooted version of SnowLeopard
 * All algorithms and designs could be found at https://www.spigotmc.org/threads/machine-learning-killaura-detection-in-minecraft.301609/
 *
 * @author Nova41
 */
public class SnowLeopardReboot extends JavaPlugin {
    /* Directory names */
    public static String DIRNAME_CATEGORY = "category";         // Directory containing all learned categories
    public static String DIRNAME_DUMPED_DATA = "dumped_data";   // Directory containing dumped angle sequence and features

    // How many features are there in our dataset; 4 currently
    public static int FEATURE_COUNT = 4;

    // Built-in neural network for category classification
    private volatile LVQNeuralNetwork neuralNetwork;

    // Map category names to an integer, because for the network, a class is an integer, not a string
    private Map<String, Integer> categoryNameMap = new HashMap<>();

    // Logger for logging angle sequence produced by players
    private PlayerAttackAngleLogger angleLogger;

    // Manager managing all sub commands of the plugin
    private CommandManager commandManager;

    public void onEnable() {
        // Initialize data folder
        try {
            SLFiles.createDirectoryIfAbsent(getDataFolder().getPath(), DIRNAME_CATEGORY);
            SLFiles.createDirectoryIfAbsent(getDataFolder().getPath(), DIRNAME_DUMPED_DATA);
            SLFiles.saveResourceIfAbsent(this, "config.yml", "config.yml");
        } catch (IOException e) {
            getLogger().severe("加载数据失败!");
            e.printStackTrace();
        }

        // Rebuild the built-in neural network with parameters specified in config.yml
        rebuildNetworkWithDataset();

        // Logger for logging angle sequence produced by players
        angleLogger = new PlayerAttackAngleLogger();
        getServer().getPluginManager().registerEvents(angleLogger, this);

        // Manager managing all sub commands of the plugin
        commandManager = new CommandManager(this, "slr");

        // Register sub commands
        registerCommands();
    }

    // Rebuild the built-in neural network with parameters specified in config.yml and dataset stored in category folder
    @SuppressWarnings("unchecked")
    private void rebuildNetworkWithDataset() {
        // Build the network with parameters specified in config.yml
        double step_size = getConfig().getDouble("LVQNN_parameters.step_size");
        double step_dec_rate = getConfig().getDouble("LVQNN_parameters.step_dec_rate");
        double min_step_size = getConfig().getDouble("LVQNN_parameters.min_step_size");
        neuralNetwork = new LVQNeuralNetwork(SnowLeopardReboot.FEATURE_COUNT, step_size, step_dec_rate, min_step_size);

        // Read dataset from category folder and train the network with the dataset
        unregisterAllCategories();

        File[] categoryFiles = new File(getDataFolder(), DIRNAME_CATEGORY).listFiles();
        if (categoryFiles == null) {
            getLogger().severe("不能读入样本数据:文件夹不存在或者拒绝访问");
            return;
        }

        if (categoryFiles.length == 0)
            return;

        for (File categoryFile : categoryFiles)
            try {
                FileConfiguration categoryFileYaml = new YamlConfiguration();
                categoryFileYaml.load(categoryFile);
                List<List<Double>> categorySamples = (List<List<Double>>) categoryFileYaml.getList("samples");

                // The network distinguish different classes with their category id (an integer)
                // So we need to map the category's name (string) to category id (an integer) for the network
                String categoryName = FilenameUtils.removeExtension(categoryFile.getName());
                if (!categoryNameMap.containsKey(categoryName))
                    registerCategory(categoryName);
                int categoryID = categoryNameMap.get(categoryName);

                // Add the parsed data to our dataset
                for (List<Double> samples : categorySamples)
                    neuralNetwork.addData(new LabeledData(categoryID, samples.stream().mapToDouble(e -> e).toArray()));

            } catch (InvalidConfigurationException | IOException e) {
                e.printStackTrace();
                getLogger().severe("不能从以下文件中读入数据: '" + categoryFile.getName() + "'");
            }

        // train asynchronously
        getServer().getScheduler().runTaskAsynchronously(this, () -> {
            int epoch = getConfig().getInt("LVQNN_train.epoch");
            neuralNetwork.normalize();
            neuralNetwork.initializeOutputLayer();

            // (not sure) i don't think neuralNetwork will change its reference during training...??
            synchronized (neuralNetwork) {
                for (int i = 0; i <= epoch - 1; i++)
                    neuralNetwork.train();
            }
        });
    }

    private void registerCommands() {
        /* Sub Commands */
        // /slr
        commandManager.register("", (sender, params) -> {
            sender.sendMessage(ChatColor.GREEN + "SnowLeopardR " + ChatColor.GOLD + " 作者: Nova41 汉化: huzpsb");
            sender.sendMessage(ChatColor.GREEN + "源代码:");
            sender.sendMessage(ChatColor.AQUA + "" + ChatColor.UNDERLINE + "https://github.com/Nova41/SnowLeopard/");
            sender.sendMessage(ChatColor.YELLOW + "/slr info" + ChatColor.WHITE + " 显示神经网络信息");
            sender.sendMessage(ChatColor.YELLOW + "/slr train <category>" + ChatColor.WHITE + " 录入一种新的特征");
            sender.sendMessage(ChatColor.YELLOW + "/slr rebuild" + ChatColor.WHITE + " 重置神经网络并且重新载入识别库");
            sender.sendMessage(ChatColor.YELLOW + "/slr reload" + ChatColor.WHITE + " 重新载入配置");
            sender.sendMessage(ChatColor.YELLOW + "/slr test <player> [时长(秒)]" + ChatColor.WHITE + " 测试一个玩家");
            sender.sendMessage(ChatColor.YELLOW + "/slr mob" + ChatColor.WHITE + " 生成一个村民给你当靶子");
            sender.sendMessage(ChatColor.YELLOW + "/slr version" + ChatColor.WHITE + " 生成一个村民给你当靶子");
            sender.sendMessage(ChatColor.YELLOW + "/slr start" + ChatColor.WHITE + " 强制开始记录一个玩家");
            sender.sendMessage(ChatColor.YELLOW + "/slr stop" + ChatColor.WHITE + " 强制结束一次录制");
            sender.sendMessage(ChatColor.YELLOW + "/slr tips" + ChatColor.WHITE + " 常见疑难问题解答");
        });

        // /slr tips
        commandManager.register("tips", ((sender, params) -> {
            sender.sendMessage(ChatColor.GREEN + "1,文件保存失败?保留字是什么?");
            sender.sendMessage(ChatColor.GOLD + "因为识别库/玩家名称和文件名相同,所以不能含有特殊字符,比如说'/'之类的.");
            sender.sendMessage(ChatColor.GREEN + "2,我一检测玩家就刷报错?");
            sender.sendMessage(ChatColor.GOLD + "请注意识别库至少需要2个.推荐3个.");
            sender.sendMessage(ChatColor.GREEN + "3,我造的识别库误判/漏判太高?");
            sender.sendMessage(ChatColor.GOLD + "请继续训练这个识别库.请放心,识别库不会被覆盖,而会被重新计算.");
            sender.sendMessage(ChatColor.GREEN + "4,我造了识别库,但是没效果?");
            sender.sendMessage(ChatColor.GOLD + "老特性了(?!).请重载神经网络.如果条件允许,请重启服务器.");
        }));

        // /slr version
        commandManager.register("version", ((sender, params) -> {
            sender.sendMessage(ChatColor.GREEN + "当前的SnowLeopardR版本:" + ChatColor.YELLOW + getDescription().getVersion());
            sender.sendMessage(ChatColor.GOLD + "作者: Nova41 汉化: huzpsb");
            sender.sendMessage(ChatColor.GREEN + "源代码: " + ChatColor.AQUA + ChatColor.UNDERLINE + "https://github.com/Nova41/SnowLeopard/");
        }));

        // /slr start
        commandManager.register("start", ((sender, params) -> {
            if (CommandValidate.notPlayer(sender)) return;

            Player player = (Player) sender;
            angleLogger.registerPlayer(player); // start logging angles
            sender.sendMessage(ChatColor.GREEN + "开始记录: " + ChatColor.YELLOW + player.getName());

        }));

        // /slr stop
        commandManager.register("stop", (sender, params) -> {
            if (CommandValidate.notPlayer(sender)) return;

            Player player = (Player) sender;
            if (!angleLogger.getRegisteredPlayers().contains(player.getName())) {
                sender.sendMessage(ChatColor.RED + "我们并不在记录: " + ChatColor.YELLOW + player.getName());
                return;
            }

            angleLogger.unregisterPlayer(player); // stop logging angles
            sender.sendMessage(ChatColor.RED + "停止记录: " + ChatColor.YELLOW + player.getName());

            // save logged angle sequence
            try {
                List<Float> angleSequence = angleLogger.getLoggedAngles(player);
                double[] extractedFeatures = SLMaths.extractFeatures(angleSequence);

                // specify the name of file containing the sequence and features extracted from it
                String saveFileName = SnowLeopardReboot.DIRNAME_DUMPED_DATA + File.separator + System.currentTimeMillis() + ".yml";
                // write the sequence and the features to the file
                File saveFile = new File(getDataFolder(), saveFileName);
                if (!saveFile.createNewFile())
                    throw new IOException();

                FileConfiguration saveFileYaml = new YamlConfiguration();
                saveFileYaml.set("feature", extractedFeatures);
                saveFileYaml.set("raw_angles", angleSequence);
                saveFileYaml.save(saveFile);

                sender.sendMessage(ChatColor.GREEN + "数据被保存在: " + ChatColor.YELLOW + saveFileName
                        + ChatColor.GREEN + " (" + angleSequence.size() + " 个数据位点).");

                // The sequence saved to disk is no longer needed to be kept in memory.
                angleLogger.clearLoggedAngles(player);
            } catch (IOException e) {
                getLogger().severe("保存数据出错.玩家: '" + player.getName() + "'");
                e.printStackTrace();
                sender.sendMessage(ChatColor.RED + "可能的原因:内部错误,磁盘已满,玩家名称含有保留字或拒绝访问.");
            }
        });

        // /slr info
        commandManager.register("info", (sender, params) -> {
            LVQNeuralNetworkSummary summary = neuralNetwork.getSummaryStatistics();
            sender.sendMessage(ChatColor.AQUA + "神经网络状态: ");
            sender.sendMessage(ChatColor.AQUA + "  数据量: " + ChatColor.YELLOW + summary.getInputCount());
            sender.sendMessage(ChatColor.AQUA + "  输出流: " + ChatColor.YELLOW + summary.getOutputCount() + " neuron(s)");
            sender.sendMessage(ChatColor.AQUA + "机器学习状态:");
            sender.sendMessage(ChatColor.AQUA + "  总任务量: " + ChatColor.YELLOW + summary.getEpoch());
            sender.sendMessage(ChatColor.AQUA + "  已经完成: " + ChatColor.YELLOW + summary.getCurrentStepSize());
            sender.sendMessage(ChatColor.AQUA + "识别模板状态:");
            sender.sendMessage(ChatColor.AQUA + "  模板数量: " + ChatColor.YELLOW + categoryNameMap.size());
            sender.sendMessage(ChatColor.AQUA + "  模板列表: ");
            categoryNameMap.forEach((cat, id) -> sender.sendMessage(ChatColor.YELLOW + "  - [" + id + "] " + cat));
        });

        // /slr train <category-name>
        commandManager.register("train", (sender, params) -> {
            if (CommandValidate.notPlayer(sender))
                return;

            // Check the number of params
            if (params.length != 1) {
                sender.sendMessage(ChatColor.RED + "错误的使用方法! /slr train <新类别名称>");
                return;
            }

            // Read configurations of sampler from config.yml. See config.yml for further information
            int duration_to_generate_a_vector = getConfig().getInt("sampler.duration_to_generate_a_vector");
            int vector_per_category = getConfig().getInt("sampler.vector_per_category");
            trainNetwork((Player) sender, params[0], duration_to_generate_a_vector, vector_per_category);
        });

        // /slr reload
        commandManager.register("reload", (sender, params) -> {
            reloadConfig();
            sender.sendMessage(ChatColor.GREEN + "配置文件已经重载.");
        });

        // /slr rebuild
        commandManager.register("rebuild", ((sender, params) -> {
            rebuildNetworkWithDataset();
            sender.sendMessage(ChatColor.GREEN + "神经网络已经重建.");
        }));

        // /slr test <player-name> <seconds>
        commandManager.register("test", (sender, params) -> {
            // Check the number of arguments
            if (params.length != 1 && params.length != 2) {
                sender.sendMessage(ChatColor.RED + "错误的使用方法! /slr test <玩家昵称> [时间(秒)]");
                return;
            }

            Player testPlayer = getServer().getPlayer(params[0]);

            // Check if the specified player is online
            if (testPlayer == null) {
                sender.sendMessage(ChatColor.RED + "以下玩家不存在: " + ChatColor.YELLOW + params[0]);
                return;
            }

            // Check if the given duration is valid
            if (params.length == 2)
                if (!StringUtils.isNumeric(params[1])) {
                    sender.sendMessage(ChatColor.YELLOW + params[1] + ChatColor.RED + " 不是一个整数.");
                    return;
                }

            // If the duration is not set, it would be the default duration specified in config.yml
            int duration = params.length == 1 ? getConfig().getInt("test.default_duration") : Integer.valueOf(params[1]);

            sender.sendMessage(ChatColor.GREEN + "开始计算玩家: " + ChatColor.YELLOW + params[0]
                    + ChatColor.GREEN + " 时间: " + ChatColor.YELLOW + duration + ChatColor.GREEN + " 秒.");
            classifyPlayer(testPlayer, duration, result -> {
                // likelihood =  max possible difference - difference
                double likelihood = SLMaths.round(result.getLikelihood() * 100, 2, RoundingMode.HALF_UP);

                sender.sendMessage(ChatColor.GREEN + "神经网络计算结果:");
                sender.sendMessage(ChatColor.GREEN + "  结论: " + ChatColor.YELLOW + getCategoryNameFromID(result.getCategory()));
                sender.sendMessage(ChatColor.GREEN + "  Δ(Ei): " + ChatColor.YELLOW + result.getDifference());
                sender.sendMessage(ChatColor.GREEN + "  可能性: " + ChatColor.YELLOW + likelihood + "%");
            });
        });

        // /slr mob
        commandManager.register("mob", (sender, params) -> {
            if (CommandValidate.notPlayer(sender))
                return;

            // Spawn a punchbag villager (having 50 hearts) in front of the player for training or practicing
            Player p = (Player) sender;
            Location spawnLoc = p.getLocation().add(p.getEyeLocation().getDirection().multiply(5));
            spawnLoc.setY(spawnLoc.getY() + 2);
            Villager villager = (Villager) p.getWorld().spawnEntity(spawnLoc, EntityType.VILLAGER);
            //villager.getAttribute(Attribute.GENERIC_MAX_HEALTH).setBaseValue(100.0D);
            villager.addPotionEffect(new PotionEffect(PotionEffectType.DAMAGE_RESISTANCE, 600, 4, true), true);
        });

        // /slr _printnn
        commandManager.register("_printnn", (sender, params) -> neuralNetwork.printStats(getLogger()));
    }

    // Train the network. Tell the network there is a category waiting to be learned from player.
    @SuppressWarnings("unchecked")
    public void trainNetwork(Player player, String category, int duration_to_generate_a_vector, int vector_per_category) {
        // Check if angle sequence of the given player is already being logged
        if (angleLogger.getRegisteredPlayers().contains(player.getName())) {
            player.sendMessage(ChatColor.RED + "内部错误:玩家身上已经存在监听器.");
            return;
        }

        // display training parameters
        player.sendMessage(ChatColor.GREEN + "开始制造识别库. ("
                + ChatColor.YELLOW + duration_to_generate_a_vector + ChatColor.GREEN + " 毫秒为基准线, "
                + ChatColor.YELLOW + vector_per_category + ChatColor.GREEN + " 个基准线为一组.)");

        /* FIRST. We need to get angle sequence of a player, and then extract features from the sequence */

        // do asynchronously
        getServer().getScheduler().runTaskAsynchronously(this, () -> {
            // vectors are stored here
            double[][] vectors = new double[vector_per_category][SnowLeopardReboot.FEATURE_COUNT];

            angleLogger.registerPlayer(player);

            // sample for the given times
            for (int i = 1; i <= vector_per_category; i++) {
                player.sendMessage(ChatColor.GREEN + "识别库建造完成度: (" + i + "/" + vector_per_category + ")");
                try {
                    // while we wait, the angleLogger is logging player's angles unweariedly
                    Thread.sleep(duration_to_generate_a_vector);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // get the angle sequence containing angles in the past duration_to_generate_a_vector milliseconds
                List<Float> angleSequence = angleLogger.getLoggedAngles(player);
                // do nothing if the player does not attack anybody in the past duration
                if (angleSequence == null)
                    break;
                // extract the features from the sequence and save them to a temporary array
                vectors[i - 1] = SLMaths.extractFeatures(angleSequence);
                // clear saved angles to get ready for new angles
                angleLogger.clearLoggedAngles(player);
            }
            angleLogger.unregisterPlayer(player);
            player.sendMessage(ChatColor.GREEN + "记录完成.正在写入文件...");

            /* SECOND. Save the vector expressing properties of the sequence to disk. */
            /*         our leopard will use these vectors to train the network */
            try {
                // get the file to be saved
                File saveFile = new File(getDataFolder(), SnowLeopardReboot.DIRNAME_CATEGORY + File.separator + category + ".yml");
                FileConfiguration saveFileYaml = new YamlConfiguration();

                List<Double[]> samplesSection = new ArrayList<>();
                // append vectors to the category if the category already exists
                if (!saveFile.createNewFile()) {
                    saveFileYaml.load(saveFile);
                    samplesSection.addAll((Collection<? extends Double[]>) saveFileYaml.getList("samples"));
                    player.sendMessage(ChatColor.GREEN + "类别: " + ChatColor.YELLOW + category + ChatColor.GREEN
                            + " 已经存在.我们正在计算融合模型.");
                }
                for (double[] vector : vectors)
                    samplesSection.add(ArrayUtils.toObject(vector));

                saveFileYaml.set("samples", samplesSection);
                saveFileYaml.save(saveFile);

                player.sendMessage(ChatColor.GREEN + "计算完毕.保存成功.恭喜!");
            } catch (IOException | InvalidConfigurationException e) {
                getLogger().severe("保存类别: '" + category + "' 时出错.");
                e.printStackTrace();
                player.sendMessage(ChatColor.RED + "可能的原因:内部错误,磁盘已满,类别名称含有保留字或拒绝访问.");
            }
        });
    }

    // Let the network guess which category does the player result in
    public void classifyPlayer(Player player, int duration, Consumer<LVQNeuralNetworkPredictResult> consumer) {
        // Check if angle sequence of the given player is already being logged
        if (angleLogger.getRegisteredPlayers().contains(player.getName())) {
            player.sendMessage(ChatColor.YELLOW + player.getName() + ChatColor.RED + " 身上已经有监听器了.");
            return;
        }

        angleLogger.registerPlayer(player);
        getServer().getScheduler().runTaskLater(this, () -> {
            List<Float> angleSequence = angleLogger.getLoggedAngles(player);
            angleLogger.unregisterPlayer(player);
            angleLogger.clearLoggedAngles(player);

            // Call the consumer with classification result
            double[] extractedFeatures = SLMaths.extractFeatures(angleSequence);
            consumer.accept(neuralNetwork.predict(extractedFeatures));
        }, duration * 20L);
    }

    // Register a category. Map a unique id to it.
    private void registerCategory(String name) {
        categoryNameMap.put(name, categoryNameMap.size());
    }

    // Convert category id from a predict result of the neural network to actual name of category
    public String getCategoryNameFromID(int id) {
        for (Map.Entry<String, Integer> entry : categoryNameMap.entrySet())
            if (entry.getValue() == id)
                return entry.getKey();
        return null;
    }

    // Clear category mappings, used when you need to reload all stored samples.
    private void unregisterAllCategories() {
        categoryNameMap.clear();
    }

    // Validation methods useful for processing commands
    private static final class CommandValidate {
        // Validate if sender is not a instance of player and notify the sender upon failed validation
        private static boolean notPlayer(CommandSender sender) {
            if (!(sender instanceof Player))
                sender.sendMessage(ChatColor.RED + "你不是一个合法的玩家.");
            return !(sender instanceof Player);
        }
    }

}
